﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Drawing;
using System.Linq;
using System.Text;
using Eksponent.CropUp.AutoDetect;
using Eksponent.CropUp.Config;
using Eksponent.CropUp.Plugins;

namespace Eksponent.CropUp
{
    public class CropUp
    {
        public static Func<Dialect> DialectResolver { get; set; }
        public static Dialect Dialect { get { return DialectResolver(); } }

        public static Func<ICropDataProvider> CropDataProviderResolver { get; set; }
        public static ICropDataProvider CropDataProvider { get { return CropDataProviderResolver(); } }


        public static CropUpSection Configuration
        {
            get { return (CropUpSection)ConfigurationManager.GetSection("cropUp") ?? new CropUpSection(); }
        }

        static CropUp()
        {
            var dialect = new ImageResizerDialect();
            DialectResolver = () => dialect;

            var provider = new DefaultCropDataProvider();
            provider.UseCache = Configuration.CacheCropData;
            if (Configuration.AutoDetect)
            {
                var auto = new FaceAndEnergyDetector(detectFaces: Configuration.DetectFaces);
                auto.BoxFaces = Configuration.BoxFaces;
                auto.FaceMargin = Configuration.FaceMargin;
                provider.AutoDetector = auto;
            }
            CropDataProviderResolver = () => provider;
        }

        public static string GetUrl(string imageRelPath, ImageSizeArguments arguments = null)
        {
            var url = Dialect.GetUrl(imageRelPath, arguments);
            if (Configuration.UseLegacyUrls)
            {
                url += ".ashx";
            }
            return url;
        }

        public int ReferenceWidth { get; set; }
        public int ReferenceHeight { get; set; }

        public CropUp(int? referenceWidth = null, int? referenceHeight = null)
        {
            ReferenceWidth = referenceWidth ?? Configuration.ReferenceWidth;
            ReferenceHeight = referenceHeight ?? Configuration.ReferenceHeight;
        }

        /// <summary>
        /// Used to suggest a default <see cref="CropInfo"/> for specific crop sizes.
        /// </summary>
        /// <param name="originalWidth">Width of the original.</param>
        /// <param name="originalHeight">Height of the original.</param>
        /// <param name="cropWidth">Width the the crop to suggest.</param>
        /// <param name="cropHeight">Height of the crop to suggest.</param>
        /// <param name="basis">The basis <see cref="CropInfo"/> too generate the suggestion from.</param>
        /// <param name="referenceWidth">Specify to override default reference width.</param>
        /// <param name="referenceHeight">Specify to override default reference height.</param>        
        /// <returns></returns>
        public CropInfo Suggest(int originalWidth, int originalHeight, int cropWidth, int cropHeight, CropInfo basis, int? referenceWidth = null, int? referenceHeight = null)
        {
            var c = GetCropping(originalWidth, originalHeight, cropWidth, cropHeight, basis, referenceWidth: referenceWidth, referenceHeight: referenceHeight);
            return new CropInfo
                {
                    Relative = false,
                    Calculated = true,
                    TopLeft = c.TopLeft,
                    BottomRight = c.BottomRight
                };
        }

        /// <summary>
        /// Returns a <see cref="Cropping"/> with the rect in the source image the target width and target height given a <see cref="CropInfo"/>.
        /// </summary>
        /// <param name="originalWidth">Width of the original image.</param>
        /// <param name="originalHeight">Height of the original image.</param>
        /// <param name="targetWidth">Target width. If not specfied the image proportions will be used.</param>
        /// <param name="targetHeight">Target height.  If not specfied the image proportions will be used.</param>
        /// <param name="info">The <see cref="CropInfo"/> that contains gravity point and crop box.</param>
        /// <param name="resizeToCropping">if set to <c>true</c> the croppings aspect ratio will be used to fill unspecified dimensions.</param>
        /// <param name="referenceWidth">Specify to override default reference width.</param>
        /// <param name="referenceHeight">Specify to override default reference height.</param>
        /// <returns></returns>
        public Cropping GetCropping(int originalWidth, int originalHeight, int? targetWidth, int? targetHeight, CropInfo info = null, bool resizeToCropping = false, int? referenceWidth = null, int? referenceHeight = null, bool allowZoom = false)
        {
            if (info == null) info = CropInfo.Default;

            info = info.ToAbsolute(originalWidth, originalHeight);

            double orgWidth = originalWidth, orgHeight = originalHeight;

            var orgAspect = orgWidth / orgHeight;

            double maxWidth = !targetWidth.HasValue ? -1 : Math.Min(allowZoom ? targetWidth.Value : originalWidth, targetWidth.Value),
                maxHeight = !targetHeight.HasValue ? -1 : Math.Min(allowZoom ? targetHeight.Value : originalHeight, targetHeight.Value);


            double cropWidth = (info.BottomRight.X - info.TopLeft.X);
            double cropHeight = (info.BottomRight.Y - info.TopLeft.Y);
            var cropAspect = cropWidth / cropHeight;

            if (!targetWidth.HasValue && !targetHeight.HasValue)
            {
                maxWidth = resizeToCropping ? cropWidth : orgWidth;
                maxHeight = resizeToCropping ? cropHeight : orgHeight;
            }
            else if (!targetWidth.HasValue)
            {
                maxWidth = (int)(maxHeight * (resizeToCropping ? cropAspect : orgAspect));
                if (!allowZoom && maxWidth > originalWidth)
                {
                    maxWidth = originalWidth;
                    maxHeight = orgWidth / (resizeToCropping ? cropAspect : orgAspect);
                }
            }
            else if (!targetHeight.HasValue)
            {
                maxHeight = (int)(maxWidth / (resizeToCropping ? cropAspect : orgAspect));
                if (!allowZoom && maxHeight > originalHeight)
                {
                    maxHeight = originalHeight;
                    maxWidth = orgHeight * (resizeToCropping ? cropAspect : orgAspect);
                }
            }

            //If zoom is used maxWidth and maxHeight are normalized to image size during calculations
            // and finalInflate is used to zoom when the method returns
            var finalInflate = 1d;
            if (allowZoom &&
                maxWidth > originalWidth || maxHeight > originalHeight )
            {
                var zx = maxWidth / originalWidth;
                var zy = maxHeight / originalHeight;
                finalInflate = Math.Max(zx, zy);
                
                if (zx > zy)
                {
                    maxWidth = originalWidth;
                    maxHeight /= zx;
                }
                else
                {
                    maxHeight = originalHeight;
                    maxWidth /= zy;
                }                
            }


            var targetAspect = maxWidth / maxHeight;

            double zoom;
            if (info.Calculated)
            {
                //Calculate zoom reduction
                var zoomedWidth = Math.Max(maxWidth, referenceWidth ?? ReferenceWidth);
                var zoomedHeight = Math.Max(maxHeight, referenceHeight ?? ReferenceHeight);

                zoom = Math.Max(zoomedHeight / orgHeight, zoomedWidth / orgWidth);
                orgWidth *= zoom;
                orgHeight *= zoom;
                cropWidth *= zoom;
                cropHeight *= zoom;
            }
            else
            {
                zoom = 1d;
            }

            double x1 = info.TopLeft.X * zoom, x2 = info.BottomRight.X * zoom, y1 = info.TopLeft.Y * zoom, y2 = info.BottomRight.Y * zoom;

            var gravity = info.Gravity.HasValue ?
                new PointF((float)(info.Gravity.Value.X * zoom), (float)(info.Gravity.Value.Y * zoom))
                : new PointF((float)(x1 + ((x2 - x1) / 2d)), (float)(y1 + ((y2 - y1) / 2d)));

            var xgravity = info.Gravity.HasValue ? 1 - (x2 - gravity.X) / (x2 - x1) : .5;
            var ygravity = info.Gravity.HasValue ? 1 - (y2 - gravity.Y) / (y2 - y1) : .5;

            if (maxWidth * zoom > cropWidth && maxHeight * zoom > cropHeight)
            {
                //Simply expand around crop area
                if (Expand(ref x1, ref x2, maxWidth * zoom, 0, orgWidth, xgravity) != 0)
                {
                    x1 = 0;
                    x2 = orgWidth;
                }
                if (Expand(ref y1, ref y2, maxHeight * zoom, 0, orgHeight, ygravity) != 0)
                {
                    y1 = 0;
                    y2 = orgHeight;
                }
            }
            else if (Math.Abs(targetAspect - cropAspect) > 0.001)
            {
                double gravitiedWidth = 0, gravitiedHeight = 0;
                if (cropAspect < targetAspect) //Grow width
                {
                    //Add image from outside the cropping box to fit target aspect ratio.
                    var newWidth = cropHeight * targetAspect;
                    var exceeded = Expand(ref x1, ref x2, newWidth, 0, orgWidth, xgravity);

                    if (exceeded > 0)
                    {
                        gravitiedWidth = orgWidth;
                        if (info.Gravity.HasValue)
                        {
                            //Keep as much context as possible                            
                            gravitiedHeight = gravitiedWidth / targetAspect;
                        }
                        else
                        {
                            //Pad rather than crop. Cropped area must be shown entirely                                                                    
                            gravitiedHeight = cropHeight;
                        }
                    }
                }
                else //Grow height
                {
                    //Add image from outside the cropping box to fit target aspect ratio.
                    var newHeight = cropWidth / targetAspect;
                    var exceeded = Expand(ref y1, ref y2, newHeight, 0, orgHeight, ygravity);

                    if (exceeded > 0) //We need to crop the the image inside the cropping box
                    {
                        gravitiedHeight = orgHeight;
                        if (info.Gravity.HasValue)
                        {
                            //Keep as much context as possible                            
                            gravitiedWidth = gravitiedHeight * targetAspect;
                        }
                        else
                        {
                            //Pad rather than crop. Cropped area must be shown entirely                            
                            gravitiedWidth = cropWidth;
                        }

                    }
                }

                if (gravitiedHeight != 0 || gravitiedWidth != 0)
                {

                    //Center around gravity point. If needed add less to lower side and more to higher. (1 - 0.5 = 1, 1 + 0.5 = 2)  

                    //x1 = gravity.X - Math.Floor((1-xgravity) * gravityedWidth);
                    //y1 = gravity.Y - Math.Floor((1-ygravity) * gravityedHeight);
                    x1 = Math.Max(0, Math.Max(gravity.X - Math.Floor(.5 * gravitiedWidth), info.TopLeft.X * zoom));
                    y1 = Math.Max(0, Math.Max(gravity.Y - Math.Floor(.5 * gravitiedHeight), info.TopLeft.Y * zoom));

                    //Keep in box
                    if (x1 + gravitiedWidth > info.BottomRight.X * zoom) x1 = Math.Max(0, info.BottomRight.X * zoom - gravitiedWidth);
                    if (y1 + gravitiedHeight > info.BottomRight.Y * zoom) y1 = Math.Max(0, info.BottomRight.Y * zoom - gravitiedHeight);


                    //Keep in image
                    if (x1 + gravitiedWidth > orgWidth) x1 = Math.Max(0, orgWidth - gravitiedWidth);
                    if (y1 + gravitiedHeight > orgHeight) y1 = Math.Max(0, orgHeight - gravitiedHeight);

                    //Set end of span
                    x2 = x1 + gravitiedWidth;
                    y2 = y1 + gravitiedHeight;

                }
            }

            return new Cropping
                {
                    TopLeft = new PointF((float)(x1 / zoom), (float)(y1 / zoom)),
                    BottomRight = new PointF((float)(x2 / zoom), (float)(y2 / zoom)),
                    TargetWidth = (int)Math.Round(maxWidth * finalInflate),
                    TargetHeight = (int)Math.Round(maxHeight * finalInflate)
                };
        }


        static double Expand(ref double lb, ref double ub, double targetWidth, double min, double max, double gravity)
        {
            var width = ub - lb;
            lb -= Math.Floor((targetWidth - width) * gravity);
            ub += Math.Floor((targetWidth - width) * (1 - gravity));

            if (ub - lb > width) --lb;

            if (lb < min)
            {
                ub += min - lb;
                lb += min - lb;
                if (ub > max)
                {
                    //Exceeds max
                    return ub - max;
                }
            }
            if (ub > max)
            {
                lb -= ub - max;
                ub -= ub - max;
                if (lb < min)
                {
                    //Exceeds min
                    return min - lb;
                }
            }

            return 0;
        }
    }
}
